/*
	Copyright (c) 2010 <copyright holders>
	This source code is licensed under The MIT License,
	see the file LICENSE for details.
	*/

#include "Unit.hpp"

#include <boost/random/uniform_int.hpp>
#include <boost/random/variate_generator.hpp>

#include "Landscape.hpp"
#include "Squad.hpp"
#include "UnitInvariant.hpp"
#include "World.hpp"

namespace spelprog2 {
namespace sim {

Unit::Pointer Unit::create(const UnitInvariant & invariant, Int playerId, Squad & squad, World & world,
    const Int2d & position, Int facing)
{
    Unit::Pointer unit(new Unit(invariant, playerId, squad, world, position, facing));
    world.getUnits().add(position, unit);
    world.getSignals().unitAdded(unit, playerId, squad.getId());

    return unit;
}

Unit::Unit(const UnitInvariant & invariant, Int playerId, Squad & squad, World & world, const Int2d & position, Int facing) :
        boost::enable_shared_from_this<Unit>(),
    world_          (world),
    invariant_      (invariant),
    targetUnit_     (),
    task_           (TASK_IDLE),
    squad_          (squad.shared_from_this()),
    target_         (position),
    position_       (position),
    facing_         (facing),
    targetFacing_   (facing),
    health_         (invariant.getMaxHealth()),
    morale_         (invariant.getMaxMorale()),
    team_           (-1),
    attackTimer_    (),
    persistTimer_   ()
{
    assert(facing.abs() <= ANGULAR_UNITS_PER_HALFCIRCLE);
}

Unit::~Unit()
{
}

Int Unit::limitAngle(Int a)
{
    if (a > ANGULAR_UNITS_PER_HALFCIRCLE) {
        a -= ANGULAR_UNITS_PER_CIRCLE;
    }
    else if (a < -ANGULAR_UNITS_PER_HALFCIRCLE) {
        a += ANGULAR_UNITS_PER_CIRCLE;
    }

    return a;
}

Int Unit::calcUnsignedAngularDifference(Int a, Int b)
{
    assert(a >= -ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(a <= ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(b >= -ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(b <= ANGULAR_UNITS_PER_HALFCIRCLE);

    Int diff = (a - b).abs();

    if (diff > ANGULAR_UNITS_PER_HALFCIRCLE) {
        diff = ANGULAR_UNITS_PER_CIRCLE - diff;
    }

    return diff;
}

Int Unit::calcSignedAngularDifference(Int a, Int b)
{
    assert(a >= -ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(a <= ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(b >= -ANGULAR_UNITS_PER_HALFCIRCLE);
    assert(b <= ANGULAR_UNITS_PER_HALFCIRCLE);

    Int diff = a - b;
    
    return limitAngle(diff);
}

Int Unit::calcDesiredFacing(const Int2d & forceSum) const
{
    //the default facing is the one given by the movement direction
    Int2d desiredDir(forceSum);

    //special cases
    if (task_ == TASK_FIGHT || task_ == TASK_HARRASS) {
        if (!targetUnit_.expired()) {
            Unit::Pointer targetUnit(targetUnit_.lock());
            if (!targetUnit->isDead()) {
                desiredDir = targetUnit->position_ - position_;
            }
        }
    }

    if (desiredDir == Int2d()) {
        //zero direction doesn't give us a clue so we go with (target) facing
        return targetFacing_;
    }

    return desiredDir.atan2();
}

void Unit::lookForFight(const UnitList & units, UnitTaskState type)
{
    Int bestTargetScore = sim::INT32_MIN_;
    for (UnitList::const_iterator it = units.begin(), end = units.end(); it != end; ++it) {
        Unit::Pointer otherUnit = *it;

        //do not attack friendly or dead units
        if (otherUnit->isDead() || otherUnit->team_ == team_) {
            continue;
        }

        Int2d toOtherUnit = otherUnit->position_ - position_;
        Int distToOtherUnit = toOtherUnit.length();

        Int score = -distToOtherUnit;
        if (score > bestTargetScore) {
            bestTargetScore = score;
            targetUnit_ = otherUnit;
            persistTimer_ = Int();
            task_ = type;
        }
    }

}

void Unit::lookForHarrass()
{
    //find maximum attack range
    Int maxRange(0);
    for (Int i(0), n(invariant_.getAttackCount()); i < n; i += Int(1)) {
        if (!invariant_.getAttack(i).firesProjectile()) {
            continue;
        }

        Int range = invariant_.getAttack(i).getRange();
        if (range > maxRange) {
            maxRange = range;
        }
    }

    if (maxRange == Int(0)) {
        return;
    }

    //find all units within max attack range (this can be expensive)
    UnitList farUnits;
    world_.getUnits().find(position_, Int2d(maxRange * Int(2), maxRange * Int(2)), *std::back_inserter(farUnits));

    lookForFight(farUnits, TASK_HARRASS);
}

void Unit::checkForPanic()
{
    //TODO: compare number of friends vs number of enemies or somesuch and modify morale
    //recall morale also is modified  by some other discrete events (like friendly units dying), but those are handled at their respective locations
}

void Unit::avoidFriends(Int2d & forceSum, const UnitList & nearUnits)
{
    const Int NORMALIZE_BIAS(256);
    const Int COLLISION_DAMPEN(250000); //roughly equal to the wrongful use of NORMALIZE_BIAS to divide down in the old code

	Int2d collisionDirSum;
    bool collision(false);

	for (UnitList::const_iterator it = nearUnits.begin(), end = nearUnits.end(); it != end; ++it) {
        Unit::ConstPointer otherUnit = *it;

        if (otherUnit->isDead()) {
            continue;
        }

        const Int2d fromOtherUnitToMe = getPosition() - otherUnit->getPosition();
        const Int distToOtherUnit = fromOtherUnitToMe.length();
		const Int collisionDist =  Int(2) * (otherUnit->invariant_.getCollisionRadius() +
            invariant_.getCollisionRadius()) - distToOtherUnit;

        if (collisionDist > Int(0)) {
			Int2d collisionDir = fromOtherUnitToMe;

            if (collisionDir != Int2d()) {
                collision = true;
			    collisionDir.normalize(NORMALIZE_BIAS);
			    collisionDirSum += collisionDir * collisionDist;
            }
		}
	}

	if (collision) {
		forceSum += collisionDirSum * invariant_.getMaxSpeed() / COLLISION_DAMPEN;
    }
}

void Unit::updatePosition(const Int2d & forceSum)
{
    const Int MAX_FACING_SPEED_REDUCTION(3);

    Int2d velocity = forceSum; //not quite physically accurate but it suffices ;)

    if (velocity != Int2d()) {
        //reduce velocity because of wrong facing (walking backwards is difficult)

        const Int optimalFacing = velocity.atan2();
        const Int facingDiff = calcUnsignedAngularDifference(optimalFacing, facing_);
        const Int denominator = ANGULAR_UNITS_PER_HALFCIRCLE / (MAX_FACING_SPEED_REDUCTION + Int(1));
        const Int reduction = facingDiff / denominator;
        assert(reduction >= Int(0));

        velocity /= Int(1) + reduction;
    }

    if (velocity != Int2d()) {
        //reduce velocity due to steepness

        const Int inclination = world_.getLandscape().getSpatials().getInclination(position_, velocity);
        if (inclination >= Int(0)) {
            //reduce speed from 75% to 0% in worst case (presume near vertical inclination is blocked regardless)
            //uses square root of inclination - means a small slope will have a greater effect proportionally than a large slope
            const Int sqrtOf90Deg = (ANGULAR_UNITS_PER_HALFCIRCLE / Int(2)).sqrt();
            const Int numerator = Int(3) * sqrtOf90Deg * (sqrtOf90Deg - inclination.sqrt());
            const Int denominator = Int(2) * ANGULAR_UNITS_PER_HALFCIRCLE;
            velocity = numerator * velocity / denominator;
        }
        else {
            //reduce speed slightly (up to 75%) if less than vertical inclination (presume near vertical inclination is blocked)
            //reduction is proportional to inclination - max reduction occurs at horizontal inclination (0 deg)
            const Int numerator = Int(2) * ANGULAR_UNITS_PER_HALFCIRCLE + inclination;
            const Int denominator = Int(2) * ANGULAR_UNITS_PER_HALFCIRCLE;
            velocity = numerator * velocity / denominator;
        }
    }

    //update/limit position

    position_ += velocity; 
    const Int2d mapExtents = world_.getLandscape().getSpatials().getExtents();
    if (position_.x < Int(0)) {
        position_.x = Int(0);
    }
    else if (position_.x >= mapExtents.x) {
        position_.x = mapExtents.x - Int(1);
    }
    if (position_.y < Int(0)) {
        position_.y = Int(0);
    }
    else if (position_.y >= mapExtents.y) {
        position_.y = mapExtents.y - Int(1);
    }
}

void Unit::updateFacing(const Int2d & forceSum)
{
    const Int desiredFacing = calcDesiredFacing(forceSum);
    Int toTurn = calcSignedAngularDifference(desiredFacing, facing_);

    if (toTurn > invariant_.getMaxTurnRate()) {
        toTurn = invariant_.getMaxTurnRate();
    }
    else if (toTurn < -invariant_.getMaxTurnRate()) {
        toTurn = -invariant_.getMaxTurnRate();
    }

    facing_ += toTurn;
    facing_ = limitAngle(facing_);
}

void Unit::calcBaseDamage(Int & healthDamage, Int & moraleDamage)
{
    typedef boost::uniform_int<sim::int32> UniformInt;
    boost::variate_generator<WorldRNG, UniformInt> baseHealthDamage(world_.getRNG(),
        UniformInt(invariant_.getAttack(0).getHealthDamageMin(), invariant_.getAttack(0).getHealthDamageMax()));
    boost::variate_generator<WorldRNG, UniformInt> baseMoraleDamage(world_.getRNG(),
        UniformInt(invariant_.getAttack(0).getMoraleDamageMin(), invariant_.getAttack(0).getMoraleDamageMax()));

    healthDamage = baseHealthDamage();
    moraleDamage = baseMoraleDamage();
}

void Unit::calcMeleeDamage(Int & healthDamage, Int & moraleDamage, const Unit & target, Int optimalAttackHeading)
{
    const Int BAD_DEFENSE_MAX_MULTIPLIER(5);

    //find damage multiplier for attack vector on enemy unit
    const Int optimalDefenseHeading = limitAngle(ANGULAR_UNITS_PER_HALFCIRCLE + optimalAttackHeading);
    const Int defenseAngleDiff = calcUnsignedAngularDifference(optimalDefenseHeading, target.facing_);
    const Int damageMultiplier = Int(1) + BAD_DEFENSE_MAX_MULTIPLIER * defenseAngleDiff / ANGULAR_UNITS_PER_HALFCIRCLE;

    calcBaseDamage(healthDamage, moraleDamage);

    healthDamage *= damageMultiplier;
    moraleDamage *= damageMultiplier;
}

void Unit::moveToTarget(Int2d & forceSum)
{
    const Int NORMALIZE_BIAS(256);

    //direction vector to target
    Int2d toTarget = target_ - position_;
    const Int distToTarget = toTarget.length();

	if (distToTarget > invariant_.getMaxSpeed()) { //uses max speed as a convenient limit for when to move back into position
		toTarget.normalize(NORMALIZE_BIAS);
        Int speed = invariant_.getMaxSpeed();
        if (task_ == TASK_IDLE) {
            speed /= Int(2);
        }
		forceSum += toTarget * speed / NORMALIZE_BIAS;
    }
    else /*if (facing_ == targetFacing_)*/ {
        task_ = TASK_IDLE;
    }
}

void Unit::fight(Int2d & forceSum)
{
    const Int NORMALIZE_BIAS(256);
    const Int MELEE_DISTANCE_TOLERANCE(100);

    if (attackTimer_ > Int(0)) {
        attackTimer_ -= 1;
    }

    if (targetUnit_.expired()) {
        task_ = TASK_IDLE;
        return;
    }

    Unit::Pointer targetUnit(targetUnit_.lock());
    if (targetUnit->isDead()) {
        task_ = TASK_IDLE;
        return;
    }

    const Int2d toTargetUnit = targetUnit->position_ - position_;

    //move towards target unit
    const Int distToMelee = toTargetUnit.length() - invariant_.getCollisionRadius() -
        targetUnit->invariant_.getCollisionRadius() - invariant_.getAttack(0).getRange();
    Int distToMove = distToMelee + MELEE_DISTANCE_TOLERANCE;
    if (distToMelee != Int(0) && toTargetUnit != Int2d()) {
        const Int approachSpeed = Int(2) * invariant_.getMaxSpeed() / Int(3);
        if (distToMove > approachSpeed) {
            distToMove = approachSpeed;
        }
        else if (distToMove < -approachSpeed) {
            distToMove = -approachSpeed;
        }

        Int2d moveDir(toTargetUnit);
        moveDir.normalize(NORMALIZE_BIAS);
        forceSum += moveDir * distToMove / NORMALIZE_BIAS;
    }

    const Int optimalAttackHeading = toTargetUnit.atan2();
    const Int attackAngleDiff = calcUnsignedAngularDifference(optimalAttackHeading, facing_);

    if (distToMelee <= Int(0) && attackAngleDiff <= invariant_.getAttack(0).getArcWidth() / Int(2)) {
        if (attackTimer_ == Int(0)) {
            //initiate attack (reset attack time)
            attackTimer_ = invariant_.getAttack(0).getTotalTime();
        }
        
        if (attackTimer_ == invariant_.getAttack(0).getTotalTime() - invariant_.getAttack(0).getEffectTime()) {
            //do damage
            Int healthDamage, moraleDamage;
            calcMeleeDamage(healthDamage, moraleDamage, *targetUnit.get(), optimalAttackHeading);
            targetUnit->damage(healthDamage, moraleDamage);
        }
    }
    else {
        persistTimer_ += 1;
        if (persistTimer_ >= invariant_.getPersistence()) {
            persistTimer_ = 0;
            task_ = TASK_IDLE;
        }
    }
}

void Unit::harrass()
{
    const Int TRIG_BIAS(256);
    const Int NORMALIZE_BIAS(256);

    assert(invariant_.getAttack(0).firesProjectile()); //temporary assumption until multiple attacks have been programmed in

    if (attackTimer_ > Int(0)) {
        attackTimer_ -= 1;
    }

    if (targetUnit_.expired()) {
        task_ = TASK_IDLE;
        return;
    }

    Unit::Pointer targetUnit(targetUnit_.lock());
    if (targetUnit->isDead()) {
        task_ = TASK_IDLE;
        return;
    }

    const Int2d toTargetUnit = targetUnit->position_ - position_;
    const Int distToTarget = toTargetUnit.length(); //don't bother dealing with collision radii here, no one will notice anyway

    if (distToTarget > invariant_.getAttack(0).getRange()) {
        return;
    }

    const Int optimalAttackHeading = toTargetUnit.atan2();
    const Int attackAngleDiff = calcUnsignedAngularDifference(optimalAttackHeading, facing_);

    if (attackAngleDiff <= invariant_.getAttack(0).getArcWidth() / Int(2)) {
        if (attackTimer_ == Int(0)) {
            //initiate attack (reset attack time)
            attackTimer_ = invariant_.getAttack(0).getTotalTime();
        }
        
        if (attackTimer_ == invariant_.getAttack(0).getTotalTime() - invariant_.getAttack(0).getEffectTime()) {
            const Int launchSpeed = invariant_.getAttack(0).getProjectile().getLaunchSpeed();
            gamelog << "launch speed: " + STRING_CAST(int(launchSpeed));
            const float numerator = PROJECTILE_GRAVITY * distToTarget;
            gamelog << "numerator: " + STRING_CAST(numerator);
            const float denominator = launchSpeed * launchSpeed;
            gamelog << "denominator: " + STRING_CAST(denominator);

            if (numerator < denominator) {
                const Int launchAngle = Int(std::asin(numerator / denominator) / FLOAT_PI * float(ANGULAR_UNITS_PER_HALFCIRCLE) * 0.5f);
                gamelog << "launch angle: " + STRING_CAST(int(launchAngle));
                const Int horizontalSpeed   = launchSpeed * launchAngle.cos(TRIG_BIAS) / TRIG_BIAS;
                const Int verticalSpeed     = launchSpeed * launchAngle.sin(TRIG_BIAS) / TRIG_BIAS;
                gamelog << "horizontal speed: " + STRING_CAST(int(horizontalSpeed));
                Int2d targetDir = toTargetUnit;
                targetDir.normalize(NORMALIZE_BIAS);
                gamelog << "vertical speed: " + STRING_CAST(int(verticalSpeed));

                const LandscapeSpatials & spatials = world_.getLandscape().getSpatials();
                const Int3d startPosition(position_.x, spatials.getAltitude(position_) + Int(2000), position_.y);
                const Int3d startVelocity(
                    horizontalSpeed * targetDir.x / NORMALIZE_BIAS,
                    verticalSpeed,
                    horizontalSpeed * targetDir.y / NORMALIZE_BIAS);

                Int healthDamage, moraleDamage;
                calcBaseDamage(healthDamage, moraleDamage);

                Projectile::create(world_, invariant_.getAttack(0).getProjectile(), startPosition, startVelocity,
                    healthDamage, moraleDamage);
            }
        }
    }
    else {
        //don't bother with persistence here - immediately go to switching target
        task_ = TASK_IDLE;
    }
}

void Unit::flee(Int2d & forceSum)
{
    //run at max speed opposite target facing for now
    const Int speed = invariant_.getMaxSpeed() * Int(4) / Int(3);
    const Int2d fleeDir(-targetFacing_.cos(speed), -targetFacing_.sin(speed));
    forceSum += fleeDir;
}

void Unit::simUpdate(Int sequenceNbr)
{
    const Int FAR_RETARGET_TIME(SIMULATION_FRAME_RATE);

    //remove self from spatial hash
    Unit::Pointer me = shared_from_this();
    world_.getUnits().remove(getPosition(), me);

    //fetch units close to this unit
    UnitList nearUnits;
    world_.getUnits().find(getPosition(), *std::back_inserter(nearUnits));

    //vector for sum of forces applied to this unit
    Int2d forceSum;

    //task based behaviour (don't try any else:s here though it could save performance, it's easier to think with if:s only)
    if (task_ != TASK_DEAD) {
        avoidFriends(forceSum, nearUnits);
    }
    if (task_ == TASK_IDLE || task_ == TASK_MOVE_TO_TARGET) {
        checkForPanic();
    }
    if (task_ == TASK_IDLE || task_ == TASK_MOVE_TO_TARGET || task_ == TASK_HARRASS) {
        lookForFight(nearUnits, TASK_FIGHT);
    }
    /*if (task_ == TASK_IDLE) {
        if (persistTimer_ <= Int(0)) {
            lookForHarrass();
            persistTimer_ = FAR_RETARGET_TIME;
        }
        else {
            persistTimer_ -= Int(1);
        }
    }*/
    if (task_ == TASK_FLEE) {
        flee(forceSum);
    }
    if (task_ == TASK_FIGHT) {
        fight(forceSum);
    }
    if (task_ == TASK_HARRASS) {
        harrass();
    }
    if (task_ == TASK_MOVE_TO_TARGET || task_ == TASK_IDLE) {
        moveToTarget(forceSum);
    }
    if (task_ != TASK_DEAD) {
        updatePosition(forceSum);
        updateFacing(forceSum);
    }

    //re-add to spatial hash and notify
    world_.getUnits().add(position_, me);
}

void Unit::setTarget(const Int2d & position, Int facing) {
    assert(facing.abs() <= ANGULAR_UNITS_PER_HALFCIRCLE);

    //TODO: consider turning this to assert instead (once there is functionality in Squad that prevents this call under these cirumstances)
    if (!readyForNewTarget()) {
        return;
    }

    target_ = position;
    targetFacing_ = facing;
    task_ = TASK_MOVE_TO_TARGET;
}

void Unit::setTarget(Unit::Pointer enemy)
{
    //TODO: consider turning this to assert instea
    if (!readyForNewTarget()) {
        return;
    }

    targetUnit_ = enemy;
    task_ = TASK_FIGHT;
    persistTimer_ = Int(0);
}

void Unit::setTeam(Int team)
{
    team_ = team;

    //TODO: update target unit if in fight
}

void Unit::damage(Int health, Int morale)
{
    health_ -= health;
    morale_ -= morale;

    if (morale_ < Int(0)) {
        morale_ = 0;
        task_ = TASK_FLEE;
    }

    if (health_ < Int(0)) {
        health_ = 0;
        task_ = TASK_DEAD;
    }
}

bool Unit::startedAttack() const
{
    return attackTimer_ == invariant_.getAttack(0).getTotalTime();
}


}; //namespace sim
}; //namespace spelprog2
